package com.monica.speaker;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.widget.Button;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static android.media.AudioTrack.PLAYSTATE_PLAYING;
import static java.lang.reflect.Array.getShort;

public class MainActivity extends AppCompatActivity {

    String TAG = "Main";
    private Handler handler;
    private AudioRecord mAudioRecord;
    private AudioTrack mAudioTrack;
    private File recordFile, playFile;
    private Button speakButton, playButton, speakMute, micMute;
    private boolean isSpeaking = false;
    private boolean isPlaying = false;

    private AudioManager audioManager;

    private ExecutorService threadPool;
    private ExecutorService playThread;

    int mBufferSizeInBytes;
    int mWriteMinBufferSize;
    MediaCodec mediaCodec;
    MediaExtractor mediaExtractor;
    int trackCount = 0;
    MediaFormat format;
    byte[] writeBuffer;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        speakButton = findViewById(R.id.speaker);
        speakButton.setOnClickListener((v) -> speak());
        playButton = findViewById(R.id.player);
        playButton.setOnClickListener((v) -> playback());
        findViewById(R.id.speaker_mute).setOnClickListener((v) -> speakMute());
        findViewById(R.id.mic_mute).setOnClickListener(v -> micMute());

        threadPool = Executors.newSingleThreadExecutor();
        playThread = Executors.newSingleThreadExecutor();

        ActivityCompat.requestPermissions(this, new String[]{
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.CAPTURE_AUDIO_OUTPUT,
                Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1001);
        init();
        handler = new Handler(Looper.myLooper());
//
    }

    private void printInfo() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
            if (mediaCodecList != null) {
                for (MediaCodecInfo info : mediaCodecList.getCodecInfos()) {
                    L.d(TAG, "codec name:" + info.getName() + ", support type:" + Arrays.toString(info.getSupportedTypes()) /*+
                            ", isHardwareAcc:" + info.isHardwareAccelerated() + ", isOnlySoft:" + info.isSoftwareOnly() + ", isVendor:" + info.isVendor()*/);
                }
            }
        }

    }


    private void micMute() {
        audioManager.setMicrophoneMute(!audioManager.isMicrophoneMute());
        sound -= 1000;
        L.d(TAG, "micMute:" + audioManager.isMicrophoneMute());
    }

    private void speakMute() {
        audioManager.setSpeakerphoneOn(!audioManager.isSpeakerphoneOn());
        sound += 1000;
        L.d(TAG, "speakMute:" + audioManager.isSpeakerphoneOn());
    }


    private void init() {
        int rate = 48000;
        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        audioManager.setMode(AudioManager.MODE_IN_CALL);

        Intent enterCall = new Intent("com.dangs.smartbox.callmode").putExtra("state", "enter");
        sendBroadcast(enterCall);

        //init audioRecord
        mBufferSizeInBytes = AudioRecord.getMinBufferSize(rate, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
        L.d(TAG, "read buff size:" + mBufferSizeInBytes);
        if (mAudioRecord == null) {
            mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, rate, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, mBufferSizeInBytes);
        }

        //init audioTrack
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            AudioAttributes mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build();
            AudioFormat audioFormat = new AudioFormat.Builder().setSampleRate(rate).setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build();
            mWriteMinBufferSize = AudioTrack.getMinBufferSize(rate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
            if (mAudioTrack == null) {
                mAudioTrack = new AudioTrack(mAudioAttributes, audioFormat, mWriteMinBufferSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
            }
        }
        writeBuffer = new byte[mBufferSizeInBytes];
        double phaseIncrement = (2 * Math.PI * 480) / rate;
        double phase = 0.0;
        for (int i = 0; i < mBufferSizeInBytes; i += 2) {
            short tem = (short) (10000 * Math.sin(phase));
            writeBuffer[i] = (byte) (tem >> 8 & 0xff);
            writeBuffer[i + 1] = (byte) (tem & 0xff);
            phase += phaseIncrement;
        }

        L.d(TAG, "write buff size:" + mWriteMinBufferSize);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mAudioRecord != null) {
            mAudioRecord.release();
        }
        if (mAudioTrack != null) {
            mAudioTrack.release();
        }
        threadPool.shutdownNow();
        playThread.shutdownNow();
        Intent enterCall = new Intent("com.dangs.smartbox.callmode").putExtra("state", "exit");
        sendBroadcast(enterCall);
    }

    private void speak() {
        if (!isSpeaking) {
            speakButton.setText(R.string.stop_speak);
            try {
                storeFile();
                threadPool.execute(() -> record());
            } catch (IOException e) {
                L.w(TAG, "make record file failed, more info:" + e.toString());
            }
        }
        isSpeaking = !isSpeaking;
    }

    private void playback() {
        if (recordFile == null || !recordFile.exists()) {
            L.w(TAG, "no record file");
            return;
        }
        if (!isPlaying) {
            playButton.setText(R.string.stop_play);
            playThread.execute(() -> play());
        }
        isPlaying = !isPlaying;
    }


    private void storeFile() throws IOException {
        File dir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
        if (!dir.exists()) {
            dir.mkdir();
        }
        SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd-hh-mm-ss");
        String date = sDateFormat.format(new java.util.Date());
        recordFile = new File("/sdcard/", date + "-R.wav");
        playFile = new File("/sdcard/", date + "-P.wav");
        recordFile.createNewFile();
        playFile.createNewFile();
        L.i(TAG, "record file store at " + recordFile.getAbsolutePath());
    }

    boolean twoSeconds = false;
    private Runnable trigelTimer = new Runnable() {
        @Override
        public void run() {
            twoSeconds = true;
            handler.postDelayed(this::run, 2000);
        }
    };


    byte sound = (byte) 128;

    private void record() {
        if (mAudioRecord == null) {
            L.w(TAG, "audio record is null");
            return;
        }
        if (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
            L.w(TAG, "audio is not init");
            return;
        }

        L.i(TAG, "audio model:" + audioManager.getMode());
        mAudioRecord.startRecording();
        L.i(TAG, "record start");
        L.i(TAG, "audio model:" + audioManager.getMode());
        if (mAudioTrack != null) {
            mAudioTrack.play();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                L.d(TAG, "max gain:" + AudioTrack.getMaxVolume());
                mAudioTrack.setVolume(1.0f);
            }
        }

        try {
//            FileWriter writer = new FileWriter(recordFile);
            FileOutputStream recordOutput = new FileOutputStream(recordFile);
            FileOutputStream playOutput = new FileOutputStream(playFile);
            byte[] readBuffer = new byte[mBufferSizeInBytes];
            byte[] tempWrite;
            byte[] silence = new byte[mBufferSizeInBytes];
            int readCount = 0;
            handler.postDelayed(trigelTimer, 500);
            long number = 0;
            while (isSpeaking) {
                readCount = mAudioRecord.read(readBuffer, 0, mBufferSizeInBytes);
                if (twoSeconds) {
                    tempWrite = writeBuffer;
                    twoSeconds = false;
                } else {
                    tempWrite = silence;
                }
                mAudioTrack.write(tempWrite, 0, readCount);
//                    mAudioTrack.write(readBuffer, 0, mBufferSizeInBytes);
                //save to file
                recordOutput.write(readBuffer);
//                playOutput.write(tempWrite);
                if (AudioRecord.ERROR_INVALID_OPERATION != readCount) {
                    for (int i = 0; i < readCount; i = i + 4) {
                        playOutput.write(writeBuffer, i, 4);
                        playOutput.write(readBuffer, i, 4);
                    }
                }
                number += readCount;
            }
            handler.removeCallbacks(trigelTimer);
            mAudioRecord.stop();
            if (mAudioTrack.getPlayState() == PLAYSTATE_PLAYING) {
                mAudioTrack.stop();
            }
            recordOutput.flush();
            recordOutput.close();
            playOutput.flush();
            playOutput.close();
            runOnUiThread(() -> speakButton.setText(R.string.start_speak));
            L.i(TAG, "record over, record number:" + number + ", play file length:" + playFile.length() + ", record file length:" + recordFile.length());
            L.i(TAG, "audio model:" + audioManager.getMode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void amp(byte[] src, int sampleByte, float amp) {
        if (src == null || src.length < 2) {
            return;
        }
        L.d(TAG, "buffer size:" + src.length + ", amp:" + amp);
        short SHRT_MAX = (short) 0x7F00;
        short SHRT_MIN = (short) -0x7F00;
        int cur = 0;
        while (cur < src.length) {
            short volume = getShort(src, cur);
//            L.d(TAG, "volume:" + volume + " at " + cur);
            volume = (short) (volume * amp);
            if (volume < SHRT_MIN) {
                volume = SHRT_MIN;
            } else if (volume > SHRT_MAX) {
                volume = SHRT_MAX;
            }
            byte high = (byte) (volume >> 8);
            byte low = (byte) (volume & 0xFF);
            Array.setByte(src, cur, high);
            Array.setByte(src, cur, low);
            cur += 2;
        }
    }

    private void play() {
        if (mAudioTrack == null) {
            return;
        }
        L.i(TAG, "play start");
        mAudioTrack.play();

        try {
            FileInputStream fileInputStream = new FileInputStream(recordFile);
            byte[] tempBuffer = new byte[mWriteMinBufferSize];
            while (fileInputStream.available() > 0 && isPlaying) {
                int readCount = fileInputStream.read(tempBuffer);
                if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                    continue;
                }
                if (readCount != 0 && readCount != -1) {
                    mAudioTrack.write(tempBuffer, 0, readCount);
                }
            }
            runOnUiThread(() -> playButton.setText(R.string.start_play));
            isPlaying = false;
            mAudioTrack.stop();
            L.i(TAG, "play over");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    void initDecode() {
        mediaExtractor = new MediaExtractor();
        try {
            mediaExtractor.setDataSource("/sdcard/test.aac");
        } catch (IOException e) {
            L.d(TAG, "get ext err:" + e.toString());
        }
        trackCount = mediaExtractor.getTrackCount();
        L.d(TAG, "tack count:" + trackCount);

        for (int i = 0; i < trackCount; i++) {
            format = mediaExtractor.getTrackFormat(i);
            try {
                mediaCodec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
            } catch (IOException e) {
                L.e(TAG, "get codec err:" + e.toString());
            }
            L.d(TAG, "format:" + format.toString());
            if (format.getString(MediaFormat.KEY_MIME).equals(MediaFormat.MIMETYPE_AUDIO_AAC)) {
                // AAC ADTS头部处理
                ByteBuffer csd = format.getByteBuffer("csd-0");
                for (int k = 0; k < csd.capacity(); ++k) {
                    L.d(TAG, "csd : " + csd.array()[k]);
                }
            }
            format = new MediaFormat();
        }
        if (mediaCodec != null) {
            MediaCodecInfo info = mediaCodec.getCodecInfo();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//                MediaCodecInfo.AudioCapabilities audioCapabilities = info.getCapabilitiesForType("audio/amr-wb").getAudioCapabilities();
                L.d(TAG, "MediaCodecInfo name:" + info.getName());
            }
        }
        decode();
        printInfo();
    }

    long TIME_OUT = 1000 * 100;

    void decode() {
        threadPool.execute(() -> {
            L.d(TAG, "start decode");
//            format.setInteger(MediaFormat.KEY_AAC_PROFILE,
//                    MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            mediaCodec.configure(format, null, null, 0);
            mediaCodec.start();
            ByteBuffer input[], output[], temp;
            input = mediaCodec.getInputBuffers();
            output = mediaCodec.getOutputBuffers();
            int inIndex, outIndex, sampleSize;
            MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
            while (true) {
                inIndex = mediaCodec.dequeueInputBuffer(TIME_OUT);
                L.d(TAG, "inIndex:" + inIndex);
                if (inIndex >= 0) {
                    temp = input[inIndex];
                    sampleSize = mediaExtractor.readSampleData(temp, 0);
                    if (sampleSize < 0) {
                        // We shouldn't stop the playback at this point, just pass the EOS
                        // flag to mediaCodec, we will get it again from the
                        // dequeueOutputBuffer
                        L.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
                        mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    } else {
                        mediaCodec.queueInputBuffer(inIndex, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
                        mediaExtractor.advance();
                    }
                    outIndex = mediaCodec.dequeueOutputBuffer(mBufferInfo, TIME_OUT);
                    L.d(TAG, "outIndex:" + outIndex + ", buffer size:" + mBufferInfo.size + ", flag:" + mBufferInfo.flags);
                    switch (outIndex) {
                        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                            L.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
                            output = mediaCodec.getOutputBuffers();
                            break;
                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                            MediaFormat format = mediaCodec.getOutputFormat();
                            L.d(TAG, "New format " + format);
                            break;
                        case MediaCodec.INFO_TRY_AGAIN_LATER:
                            L.d(TAG, "dequeueOutputBuffer timed out!");
                            break;
                        default:
                            ByteBuffer outBuffer = output[outIndex];
                            L.v(TAG, "We can't use this buffer but render it due to the API limit, " + outBuffer);
                            final byte[] chunk = new byte[mBufferInfo.size];
                            outBuffer.get(chunk); // Read the buffer all at once
                            outBuffer.clear(); // ** MUST DO!!! OTHERWISE THE NEXT TIME YOU GET THIS SAME BUFFER BAD THINGS WILL HAPPEN
                            MediaFormat mFormat = mediaCodec.getOutputFormat();
//                            mOnCapturePCMListener.capturePCM(chunk, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mFormat.getInteger
//                                    (MediaFormat.KEY_CHANNEL_COUNT));
                            mediaCodec.releaseOutputBuffer(outIndex, false);
                            break;
                    }
                    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        L.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
                        break;
                    }
                }
            }
            L.d(TAG, "decode done");
            mediaCodec.stop();
            mediaCodec.release();
            mediaExtractor.release();
            threadPool.shutdownNow();
        });
    }

    public void decode2() {
        threadPool.execute(() -> {
            mediaCodec.configure(format, null, null, 0);
            mediaCodec.start();
            ByteBuffer[] mInputByteBuffers = mediaCodec.getInputBuffers();
            ByteBuffer[] mOutputByteBuffers = mediaCodec.getOutputBuffers();
            MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
            while (true) {
                int inIndex = mediaCodec.dequeueInputBuffer(TIME_OUT);
                if (inIndex >= 0) {
                    ByteBuffer buffer = mInputByteBuffers[inIndex];
                    int sampleSize = mediaExtractor.readSampleData(buffer, 0);
                    if (sampleSize < 0) {
                        // We shouldn't stop the playback at this point, just pass the EOS
                        // flag to mediaCodec, we will get it again from the
                        // dequeueOutputBuffer
                        L.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
                        mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    } else {
                        mediaCodec.queueInputBuffer(inIndex, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
                        mediaExtractor.advance();
                    }
                    int outIndex = mediaCodec.dequeueOutputBuffer(mBufferInfo, TIME_OUT);
                    L.d(TAG, "outIndex:" + outIndex + ", buffer size:" + mBufferInfo.size + ", flag:" + mBufferInfo.flags);
                    switch (outIndex) {
                        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                            L.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
                            mOutputByteBuffers = mediaCodec.getOutputBuffers();
                            break;
                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                            MediaFormat format = mediaCodec.getOutputFormat();
                            L.d(TAG, "New format " + format);
                            break;
                        case MediaCodec.INFO_TRY_AGAIN_LATER:
                            L.d(TAG, "dequeueOutputBuffer timed out!");
                            break;
                        default:
                            ByteBuffer outBuffer = mOutputByteBuffers[outIndex];
                            L.v(TAG, "We can't use this buffer but render it due to the API limit, " + outBuffer);
                            final byte[] chunk = new byte[mBufferInfo.size];
                            outBuffer.get(chunk); // Read the buffer all at once
                            outBuffer.clear(); // ** MUST DO!!! OTHERWISE THE NEXT TIME YOU GET THIS SAME BUFFER BAD THINGS WILL HAPPEN
                            MediaFormat mFormat = mediaCodec.getOutputFormat();
//                        mOnCapturePCMListener.capturePCM(chunk, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mFormat.getInteger
//                                (MediaFormat.KEY_CHANNEL_COUNT));
                            mediaCodec.releaseOutputBuffer(outIndex, false);
                            break;
                    }
                    // All decoded frames have been rendered, we can stop playing now
                    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        L.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
                        break;
                    }
                }
            }
            mediaCodec.stop();
            mediaCodec.release();
            mediaCodec = null;
            mediaExtractor.release();
            mediaExtractor = null;
            threadPool.shutdown();
            threadPool = null;
        });

    }


}
